#!/usr/bin/python3
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*-

import os
import re
import pwd
import glob
import time
import stat
import json
import procfs
import ntplib
import pathlib
import filecmp
import gstage4
import subprocess
import strict_pgs
import strict_fsh
import strict_pam
import strict_hdds
import strict_cups
import configparser
import strict_hwcfg
import strict_portage
import strict_systemd
import strict_nsswitch
from fm_util import Util
from fm_util import CcacheLocalService
from fm_util import PhysicalDiskMounts
from fm_util import TmpMount
from fm_param import FmConst
from helper_bbki import BbkiWrapper
from helper_bbki import BbkiCfgFileUtil
from helper_swap import SwapManager
from helper_pkg_warehouse import PkgWarehouse
from helper_pkg_warehouse import EbuildRepositories
from helper_pkg_warehouse import EbuildOverlays
from helper_pkg_warehouse import RepositoryCheckError
from helper_pkg_warehouse import OverlayCheckError
from helper_pkg_warehouse import CloudOverlayDb
from helper_pkg_warehouse import PkgMerger
from stage4_gentoo import FpemudOs


# TODO:
# 1. no .config/osinfo should exist
# 3. ssd io scheduler check
# 7. systemd unit files reference not-exist service or target file
# 8. check mount option for boot device
# 9. should not have uid/gid without name
# *. remove "default" routing table from /etc/iproute2/rt_tables, remove "32767:  from all lookup default" rule (is it set by kernel?)
# *. hardware check: monitor config by ddcci
# *. install and uninstall all in_focus packages (ensure they are installable)
# *. don't enable non-critical INTEL and AMD cpu kernel option (it's a general TODO item, not related to sysman-check)
# *. check dmesg for "end_request: I/O error, dev 03:06 (sda), sector"
# *. check dmesg for "nvme0n1: Read(0x2) @ LBA 294181576, 256 blocks, Unrecovered Read Error (sct 0x2 / sc 0x81) DNR"
# *. check dmesg for "critical medium error, dev nvme0n1, sector 294181576 op 0x0:(READ) flags 0x4000 phys_seg 2 prio class 3"
# *. mounting everything except /dev with the nodev mount option

# *. verify cups setup is fresh and easy
#    if printer is exported:
#       non-network printer exist and only non-network printer can be exported
#    if printer is not exported:
#       do nothing
# *. no virtual-printer is allowed. virtual-printer is a mis-use of print framework, they should be implemented as other forms

# *. no abstract unix domain socket should exist (https://utcc.utoronto.ca/~cks/space/blog/linux/SocketAbstractNamespace?showcomments, comment's from Lennart)
# *. no direcotry is mounted more than once
# *. check geolocation and timezone matchness. no auto-fix, timezone should be auto-updated.
# *. check no realtime process or at least only kernel threads can be realtime.

# *. noop i/o scheduler should be used when I'm a VM
# *. bfq should be used for all harddisk (http://phoronix.com/scan.php?page=article&item=linux-420-io&num=3), low_latency should be enabled
# *. scsi_mod.use_blk_mq=1   what is it?

# *. btrfs-scrub
# *. btrfs find duplicate blocks
# *. btrfs check fragmentation status
# *. disable copy-on-write for single files/directories

# FIXME:
# *. for Sony 1000-MX3 headset: only use A2DP + AVCRP profile. don't use HSP/HFP profile.
# *.                            don't use it's microphone, use the microphone in camera.
# *. enable mSBC, SBC-XQ codec in pipewire? why not enable by default? won't auto downgrade to CVSD?
# *. enable bluez experimental API?
# *. check battery level (we don't use upower to display battery level in realtime), or use richman daemon?
# *. only use bluez "bredr"? "dual" and "le" is not stable?
# *. disable LE
# *. enable bluez5.hw-volume [hfp_hfs hsp_hs ad2p_sink hfp_ag, hsp_ag, a2dp_source]?

# *. no RootDirectory in systemd service files
# *. no ReadOnlyDirectories and InaccessibleDirectories in systemd service files

# *. no .mount service
# *. no .automount service

# shared-mime-info extra files:
# *. ~/.local/share/mime/
# *. mimeapps.list in many locations
# *. ~/.local/share/applications/mimeapps.list, ~/.local/share/applications/defaults.list symlink?
# *. desktop-mimeapps.list in many locations

# use ~/.config/git/config, don't use ~/.gitconfig

# all processes within a systemd service should belong to one process group, and the systemd service main process should be group leader
# all processes within a systemd service should belong to one session? systemd service main process should be session leader?

# no xwayland process is allowed (disable xwayland totally instead?)
# RPATH is disallowed in all elf files (for some directories or the whole system?)

# *. validate all user written desktop files using desktop-file-validate executable

# bash should not be login shell (--login), login shell means bash is session manager
# we should only use usrman-session as session manager

#
# exception rules:
# 1. use "printError" than "raise exception" if possible
# 2. no "printError" in basicCheck()


class FmSysChecker:

    def __init__(self, param):
        self.param = param
        self.fpemudOs = FpemudOs()

    def init(self, bInit, bAutoFix):
        assert not hasattr(self, "bInit")

        self.bInit = bInit
        self.bAutoFix = bAutoFix
        self.infoPrinter = None

    def basicCheck(self, repositoryOverlayCheck=False):
        self._checkNotSystemdFirstBoot()

        self._checkSeatCfg()

        machine = strict_hwcfg.probe()
        try:
            self._checkPortageCfg(machine, bFullCheck=False)
        except FmCheckException:
            if self.bInit:
                # auto fix aggressively, then check again
                if True:
                    Util.forceDelete(os.path.join(FmConst.portageCfgUseDir, "99-autouse"))
                    Util.removeDirContentExclude(FmConst.portageCfgReposDir, [])
                self._checkPortageCfg(machine, bFullCheck=False)

        if repositoryOverlayCheck:
            try:
                self._checkRepositories(bFullCheck=False)
            except FmCheckException:
                if self.bInit:
                    # auto fix aggressively, then check again
                    Util.removeDirContentExclude(FmConst.portageCfgReposDir, [])
                    self._checkRepositories(bFullCheck=False)
            try:
                self._checkOverlays(bFullCheck=False)
            except FmCheckException:
                if self.bInit:
                    # auto fix aggressively, then check again
                    if True:
                        repoman = EbuildRepositories()
                        repoCfgFnList = [os.path.basename(repoman.getRepoCfgReposFile(x)) for x in repoman.getRepositoryList()]
                        Util.removeDirContentExclude(FmConst.portageCfgReposDir, repoCfgFnList)
                    self._checkOverlays(bFullCheck=False)

    def fullCheck(self, deepHardwareCheck, deepFileSystemCheck):
        self.infoPrinter = self.param.infoPrinter
        try:
            machine = None
            layout = None
            bbkiObj = None

            with self.infoPrinter.printInfoAndIndent(">> Preparing..."):
                self._checkNotSystemdFirstBoot()
                machine = strict_hwcfg.probe()
                layout = strict_hdds.get_storage_layout()

            with self.infoPrinter.printInfoAndIndent(">> Checking hardware..."):
                if True:
                    # sub-title is dynamically printed in self._checkHarddisks()
                    self._checkHarddisks(layout.get_disk_list(), deepHardwareCheck)
                with self.infoPrinter.printInfoAndIndent("- Check cooling system..."):
                    self._checkCooling()
                with self.infoPrinter.printInfoAndIndent("- Check hardware configuration..."):
                    machine.check(auto_fix=self.bAutoFix, error_callback=self.infoPrinter.printError)
                with self.infoPrinter.printInfoAndIndent("- Check harddisk write safety..."):
                    self._checkHddWriteSafety(machine, layout)

            with self.infoPrinter.printInfoAndIndent(">> Checking storage..."):
                with self.infoPrinter.printInfoAndIndent("- Check storage layout"):
                    self._checkStorageLayout(layout)
                with self.infoPrinter.printInfoAndIndent("- Check swap"):
                    sm = SwapManager(layout, self.param.runMode != "normal")
                    sm.check(self.bAutoFix, self.infoPrinter.printError)
                with self.infoPrinter.printInfoAndIndent("- Check rootfs..."):
                    self._checkRootfsLayout(deepFileSystemCheck)
                with self.infoPrinter.printInfoAndIndent("- Check premount rootfs..."):
                    self._checkPreMountRootfsLayout()

            with self.infoPrinter.printInfoAndIndent(">> Checking BIOS, bootloader, kernel and initramfs..."):
                bbkiObj = BbkiWrapper(machine, layout=layout)
                with self.infoPrinter.printInfoAndIndent("- Check config..."):
                    bbkiObj.check_config(auto_fix=self.bAutoFix, error_callback=self.infoPrinter.printError)
                with self.infoPrinter.printInfoAndIndent("- Check boot entries..."):
                    if self.bAutoFix:
                        with strict_hdds.BootDirWriter(layout):
                            bbkiObj.check_boot_entry_files(auto_fix=self.bAutoFix, error_callback=self.infoPrinter.printError)
                    else:
                        bbkiObj.check_boot_entry_files(auto_fix=self.bAutoFix, error_callback=self.infoPrinter.printError)

            with self.infoPrinter.printInfoAndIndent(">> Checking operating system..."):
                with self.infoPrinter.printInfoAndIndent("- Check system configuration..."):
                    if True:
                        self._checkEtcOnlyUserCreatedFiles()    # regulation in /etc
                    if True:
                        # self._checkCoredumpCfg()              # config in /proc
                        self._checkCpuFreqDriver()              # config in /sys
                        self._checkSeatCfg()                    # config in /etc
                        self._checkHostsFile()                  # config in /etc
                        self._checkNsswitchFile()               # config in /etc
                        self._checkSystemLocale()               # config in /etc
                        # self._checkPamCfgFiles()              # config in /etc
                        self._checkEtcLmSensorsCfgFiles()       # config in /etc
                        self._checkEtcUdevRuleFiles()           # config in /etc
                        self._checkCcacheFilesAndDirectories()  # config in /etc
                        self._checkPortageCfg(machine)          # config in /etc
                        self._checkCupsCfg()                    # config in /etc
                        self._checkSystemServiceEnablement()    # config in /etc
                        self._checkSystemService()              # config in /etc
                        self._checkMrget()                      # config in /etc
                    if True:
                        self._checkSystemTime()                 # dynamic system status
                        self._checkUserLinger()
                        self._checkNoKmodStaticNodes()
                        self._checkTmpFiles()
                        # self._checkEnvironmentVariables()       # dynamic system status
                        self._checkPkgConfig()
                        self._checkBarkspider()
                with self.infoPrinter.printInfoAndIndent("- Check package repositories & overlays..."):
                    self._checkRepositories()
                    self._checkOverlays()
                    self._checkPortagePkgwhCfg(machine)
                    self._checkNews()
                    self._checkWorldFile()
                    self._checkRedundantRepositoryAndOverlay()
                with self.infoPrinter.printInfoAndIndent("- Check users and groups..."):
                    self._checkSysUsers()
                    self._checkUsersAndGroups()

            with self.infoPrinter.printInfoAndIndent(">> Checking software packages..."):
                for pkgNameVer in sorted(Util.portageGetInstalledPkgAtomList(FmConst.portageDbDir)):
                    with self.infoPrinter.printInfoAndIndent("- Package %s:" % (pkgNameVer), bRecallable=True):
                        # self._checkPackageContentFile(machine, pkgNameVer)
                        self._checkPackageFileScope(pkgNameVer)
                        # self._checkPackageMd5(pkgNameVer)
        finally:
            del self.infoPrinter

    def _checkNotSystemdFirstBoot(self):
        # systemd does extra work which is more of a hindrance than a help when it thinks this is the first boot
        # systemd definitely will be replaced

        if not os.path.exists("/etc/machine-id"):
            if self.bAutoFix:
                with open("/etc/machine-id", "w") as f:
                    f.write(Util.generateRandomMachineId() + "\n")
            else:
                raise Exception("FATAL: systemd first boot detected! /etc/machine-id does not exist.")

        if Util.isMountPoint("/etc/machine-id"):
            raise Exception("FATAL: systemd first boot detected! /etc/machine-id is a mount point.")

        machineId = pathlib.Path("/etc/machine-id").read_text().rstrip("\n")
        if not Util.checkMachineId(machineId):
            if self.bAutoFix:
                with open("/etc/machine-id", "w") as f:
                    f.write(Util.generateRandomMachineId() + "\n")
            else:
                raise Exception("FATAL: systemd first boot detected! /etc/machine-id has invalid content")

    def _checkSeatCfg(self):
        rulesDir = "/etc/udev/rules.d"
        if not os.path.exists(rulesDir):
            if self.bAutoFix:
                os.makedirs(rulesDir)
            else:
                raise Exception("no seat udev rules file exists.")

        tlist = [(fn, fullfn) for fn, fullfn in Util.listDirWithoutKeepFiles(rulesDir) if fn.startswith("72-")]
        if len(tlist) == 0:
            if self.bAutoFix:
                with open(os.path.join(rulesDir, "72-seat-main.rules"), "w") as f:
                    f.write('TAG=="seat", ENV{ID_SEAT}="seat-main"\n')
            else:
                raise Exception("no seat udev rules file exists.")
        elif len(tlist) > 1:
            # no multi-seat
            # multi-seat is broken on linux because there's no way to *hide* device from a seat
            # and there's no way to restrict access to device for a seat (can restrict only for a user, process of seat A can access device of seat B when a user logins to both seats).
            raise Exception("multiple seat udev rules files exist.")

    def _checkHarddisks(self, diskList, deepCheck):
        # hardware check
        if not deepCheck:
            for hdd in diskList:
                with self.infoPrinter.printInfoAndIndent("- Doing basic hardware check for %s(%s)" % (hdd, Util.getBlkDevModel(hdd))):
                    rc, out = Util.cmdCallWithRetCode("smartctl", "-H", hdd)
                    if re.search("FAILED", out, re.I) is not None:
                        self.infoPrinter.printError("HDD health check failed! Run \"smartctl -H %s\" to do future inspection!" % (hdd))
        else:
            with self.infoPrinter.printInfoAndIndent("- Starting extensive hardware test for %s(%s)" % (hdd, Util.getBlkDevModel(hdd))):
                tlist2 = list(diskList)
                for hdd in diskList:
                    try:
                        rc, out = Util.cmdCallWithRetCode("smartctl", "-t", "long", hdd)
                        if rc == 0:
                            m = re.search("Please wait ([0-9]+) minutes for test to complete\\.", out, re.M)
                            if m is None:
                                raise Exception("")
                            self.infoPrinter.printInfo("Test on %s(%s) started, %s minutes needed." % (hdd, Util.getBlkDevModel(hdd), m.group(1)))
                        elif rc == 4:
                            self.infoPrinter.printInfo("Test on %s(%s) started. Why it is already in progress?" % (hdd, Util.getBlkDevModel(hdd)))
                        else:
                            raise Exception("")
                    except:
                        self.infoPrinter.printError("Failed to start test on %s(%s)!" % (hdd, Util.getBlkDevModel(hdd)))
                        subprocess.run(["smartctl", "-X", hdd], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                        tlist2.remove(hdd)

            with self.infoPrinter.printInfoAndIndent("- Waiting..."):
                try:
                    last_progress = 0
                    while tlist2 != []:
                        time.sleep(60 * 5)
                        min_progress = None
                        for hdd in list(tlist2):
                            out = Util.cmdCall("smartctl", "-l", "selftest", hdd)
                            if re.search("# 1\\s+Extended offline\\s+Completed without error\\s+.*", out, re.M) is not None:
                                self.infoPrinter.printInfo("Test on %s finished." % (hdd))
                                tlist2.remove(hdd)
                                continue
                            m = re.search("# 1\\s+Extended offline\\s+Self-test routine in progress\\s+([0-9]+)%.*", out, re.M)
                            if m is None:
                                self.infoPrinter.printInfo("Test on %s failed. Run \"smartctl -l selftest %s\" to do future inspection." % (hdd, hdd))
                                tlist2.remove(hdd)
                                continue
                            if min_progress is None:
                                min_progress = 100
                            min_progress = min(min_progress, 100 - int(m.group(1)))
                        if min_progress is not None and min_progress > last_progress:
                            self.infoPrinter.printInfo("Test progress: %d%%" % (min_progress))
                            last_progress = min_progress
                finally:
                    for hdd in tlist2:
                        subprocess.run(["smartctl", "-X", hdd], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    def _checkCooling(self):
        # FIXME: check temperature event, too high, cpu throttle, gpu throttle... (_checkCooling)

        # check cpu thermal throttle history
        for fullfn in glob.glob("/sys/devices/system/cpu/cpu*/thermal_throttle/*_throttle_count"):
            count = int(pathlib.Path(fullfn).read_text().rstrip("\n"))
            if count != 0:
                self.infoPrinter.printError("\"%s\" is not 0." % (fullfn))

    def _checkHddWriteSafety(self, machine, layout):
        for devPath in layout.get_disk_list():
            c = Util.hwcfgGetComponentByUdevDeviceNode(machine, strict_hwcfg.ComponentType.HARDDISK, devPath)
            if c is not None:
                if c.get_write_safety() == "safe":
                    pass
                elif c.get_write_safety() == "safe-uncached":
                    if Util.getBlkDevHardwareWriteCacheStatus(devPath):
                        # no way to auto-fix, it's not enough to simply disable it, it must be set on boot
                        self.infoPrinter.printError("\"%s\" is unsafe." % (devPath))
                elif c.get_write_safety() == "unsafe":
                    self.infoPrinter.printError("\"%s\" is unsafe." % (devPath))
                else:
                    assert False
            else:
                raise Exception("harddisk \"%s\" has no hardware configuration" % (devPath))

    def _checkStorageLayout(self, layout):
        def __errCb(checkCode, message):
            self.infoPrinter.printError(message)

        layout.check(auto_fix=self.bAutoFix, error_callback=__errCb)
        if layout.name in ["bios-ext4", "efi-ext4"]:
            layout.opt_check("swap", auto_fix=self.bAutoFix, error_callback=__errCb)
        if layout.name == "efi-bcache-btrfs":
            layout.opt_check("bcache-write-mode", "writeback", auto_fix=self.bAutoFix, error_callback=__errCb)
        if True:
            layout.opt_check("mount-write-mode", auto_fix=False, error_callback=__errCb)    # we don't do auto-fix here, user may want to keep this state

        disallowedMntOpts = ["atime", "relatime", "nodiratime"]
        for mntEntry in PhysicalDiskMounts.get_entries():
            for mo in disallowedMntOpts:
                if mo in mntEntry.mnt_opt_list:
                    self.infoPrinter.printError("%s should not be enabled for mount point \"%s\"." % (mo, mntEntry.mountpoint))

    def _checkRootfsLayout(self, deepCheck):
        # file system layout check
        obj = strict_fsh.RootFs()
        obj.check(deep_check=deepCheck, auto_fix=self.bAutoFix, error_callback=self.infoPrinter.printError)

        # /usr/local should not exist
        if os.path.exists("/usr/local"):
            self.infoPrinter.printError("/usr/local should not exist.")

        # check /etc/sandbox.conf, or else some ebuild files automatically create /usr/local with no file in it
        try:
            fullfn = "/etc/sandbox.conf"
            buf = pathlib.Path(fullfn).read_text()
            buf = "\n" + buf + "\n"
            if '\nSANDBOX_PREDICT="/usr/local"\n' not in buf:
                if self.bAutoFix:
                    buf = buf.strip("\n") + '\nSANDBOX_PREDICT="/usr/local"\n'
                    pathlib.Path(fullfn).write_text(buf)
                else:
                    self.infoPrinter.printError('Directory "/usr/local" in not configured in "%s".' % (fullfn))
        except FileNotFoundError:
            pass

    def _checkPreMountRootfsLayout(self):
        mRootfs = PhysicalDiskMounts.find_root_entry()
        with TmpMount(mRootfs.device, options=mRootfs.opts) as mp:
            obj = strict_fsh.PreMountRootFs(mp.mountpoint,
                                            mounted_boot=(PhysicalDiskMounts.find_entry_by_mount_point("/boot") is not None),
                                            mounted_etc=False,
                                            mounted_home=(PhysicalDiskMounts.find_entry_by_mount_point("/root") is not None),
                                            mounted_var=(len(PhysicalDiskMounts.find_entries_by_filter(lambda x: x.mountpoint.startswith("/var/"))) > 0))
            obj.check(auto_fix=self.bAutoFix, error_callback=self.infoPrinter.printError)

    def _checkCoredumpCfg(self):
        out = subprocess.check_output(os.path.join(FmConst.libexecDir, "coredump-register.sh"), text=True)
        if out != "":
            # no auto-fix should be done, need reboot
            for line in out.split("\n"):
                if line != "":
                    self.infoPrinter.printError(line)

        fullfn = "/proc/sys/fs/suid_dumpable"
        if pathlib.Path(fullfn).read_text() != "0":
            # no auto-fix should be done, it may be changed for debugging purpose
            self.infoPrinter.printError("%s should be 0." % (fullfn))

    def _checkCpuFreqDriver(self):
        # hwInfo = self.param.machineInfoGetter.hwInfo()

        # drv = None
        # if hwInfo.hwDict["cpu"]["vendor"] == "Intel":
        #     drv = "intel_p_state"
        # elif hwInfo.hwDict["cpu"]["vendor"] == "AMD":
        #     drv = "acpi_freq"
        # else:
        #     self.infoPrinter.printError("CPU vendor is %s, can not check cpu-freq driver.")
        #     return

        # flist = Util.shellCall("find /sys/devices/system/cpu/cpufreq -name scaling_driver").split("\n")
        # flist = [x for x in flist if x != ""]
        # if len(flist) != hwInfo.hwDict["cpu"]["cores"]:
        #     self.infoPrinter.printError("Incorrent number of scaling_driver files (%d/%d) found in sysfs." % (hwInfo.hwDict["cpu"]["cores"], len(flist)))

        # for fullfn in flist:
        #     if pathlib.Path(fullfn).read_text() != drv + "\n":
        #         self.infoPrinter.printError("Invalid content in file \"%s\"." % (fullfn))

        pass

    def _checkHostsFile(self):
        """Check /etc/hosts"""

        content = ""
        content += "127.0.0.1 localhost\n"
        content += "::1 localhost\n"                    # selenium fails when "::1 localhost" exist in /etc/hosts ?
        if pathlib.Path("/etc/hosts").read_text() != content:
            if self.bAutoFix:
                pathlib.Path("/etc/hosts").write_text(content)
            else:
                self.infoPrinter.printError("File /etc/hosts has invalid content.")

    def _checkNsswitchFile(self):
        """Check /etc/nsswitch.conf"""

        obj = strict_nsswitch.NSSwitchConf()

        bFixed = obj.check(auto_fix=self.bAutoFix, error_callback=self.infoPrinter.printError)

        # hosts
        while True:
            if not obj.has_database("hosts"):
                if self.bAutoFix:
                    obj.add_database("hosts")
                else:
                    self.infoPrinter.printError("no database \"hosts\" in \"%s\"" % (obj.path))
                    break

            if "sys-auth/nss-mdns" in strict_portage.cfg.World().get_package_names():
                line = "hosts: files myhostname mdns_minimal dns"
            else:
                line = "hosts: files myhostname dns"
            if obj.format_line("hosts") != line:
                if self.bAutoFix:
                    obj.set_database_by_line(line)
                    bFixed = True
                else:
                    self.infoPrinter.printError("services are invalid for database \"hosts\" in \"%s\"" % (obj.path))
            break

        if bFixed:
            obj.save()

    def _checkPamCfgFiles(self):
        context = strict_pam.Context()

        goodDict = {}
        for serviceName in context.get_service_names():
            try:
                goodDict[serviceName] = context.get_service(serviceName)
            except strict_pam.ParseError as e:
                raise FmCheckException(str(e))
            except strict_pam.VerifyError as e:
                self.infoPrinter.printError(str(e))

        def __sDict(tlist):
            for serviceName, sobj in goodDict.items():
                if serviceName in tlist:
                    yield serviceName, sobj

        clsRootOk = strict_pam.features.auth_acc.RootOk
        clsWheelOk = strict_pam.features.auth_acc.WheelOk
        clsLimits = strict_pam.features.session.Limits
        clsXAuth = strict_pam.features.session.XAuth

        # check all PAM services
        for serviceName, sobj in goodDict.items():
            # check pam_limits.so
            if sobj.get_feature(clsLimits) is not None:
                if self.bAutoFix:
                    sobj.remove_feature(clsLimits)
                else:
                    self.infoPrinter.printError("PAM service \"%s\" should not have feature \"%s\"" % (serviceName, clsLimits.name()))

            # check pam_xauth.so
            if sobj.get_feature(clsXAuth) is not None:
                if self.bAutoFix:
                    sobj.remove_feature(clsXAuth)
                else:
                    self.infoPrinter.printError("PAM service \"%s\" should not have feature \"%s\"" % (serviceName, clsXAuth.name()))

        # check specific PAM services
        for serviceName, sobj in __sDict(["su", "su-l"]):
            # pam_rootok.so
            if sobj.get_feature(clsRootOk) is None:
                if self.bAutoFix:
                    sobj.add_feature(clsRootOk)
                else:
                    self.infoPrinter.printError("PAM service \"%s\" does not have feature \"%s\"" % (serviceName, clsRootOk.name()))

            # pam_wheel.so
            if sobj.get_feature(clsWheelOk) is None:
                if self.bAutoFix:
                    sobj.remove_feature(strict_pam.features.auth_acc.MustBeWheel)
                    sobj.add_feature(clsWheelOk)
                else:
                    self.infoPrinter.printError("PAM service \"%s\" does not have feature \"%s\"" % (serviceName, clsWheelOk.name()))

            # base on
            if serviceName == "su-l":
                if sobj.get_based_on() != "su":
                    # no way to auto-fix
                    self.infoPrinter.printError("PAM service \"%s\" should base on \"su\"" % (serviceName))

        if self.bAutoFix:
            for serviceName, sobj in goodDict.items():
                sobj.save()

    def _checkEtcOnlyUserCreatedFiles(self):
        # the following directories have /usr/lib/* counterpart, so only user created files are allowed in them
        dirList = [
            "/etc/NetworkManager/dispatcher.d"
            "/etc/binfmt.d",
            "/etc/environment.d",
            "/etc/modprobe.d",
            "/etc/modules-load.d",
            "/etc/systemd/system",
            "/etc/systemd/user",
            "/etc/tmpfiles.d",
            "/etc/udev/hwdb.d",
            "/etc/udev/rules.d",
        ]

        fileSet = None
        for dn in dirList:
            for fullfn in glob.glob(os.path.join(dn, "*")):
                if fileSet is None:
                    fileSet = Util.portageGetInstalledFileSet()
                if fullfn in fileSet:
                    self.infoPrinter.printError("\"%s\" should contain only user created files, but \"%s\" is not." % (dn, fullfn))

    def _checkEtcLmSensorsCfgFiles(self):
        fn = "/etc/modules-load.d/lm_sensors.conf"
        if not os.path.exists(fn):
            self.infoPrinter.printError("You should use \"sensors-detect\" command from package \"sys-apps/lm-sensors\" to generate \"%s\"." % (fn))

    def _checkEtcUdevRuleFiles(self):
        # check /etc/udev/hwdb.d
        hwdbDir = "/etc/udev/hwdb.d"
        if os.path.exists(hwdbDir):
            for _, fullfn in Util.listDirWithoutKeepFiles(hwdbDir):
                self.infoPrinter.printError("\"%s\" should be empty, so \"%s\" should not exist." % (hwdbDir, fullfn))

        # check /etc/udev/rules.d
        rulesDir = "/etc/udev/rules.d"
        if os.path.exists(rulesDir):
            for fn, fullfn in Util.listDirWithoutKeepFiles(rulesDir):
                if fn == "72-seat-main.rules":
                    continue
                if not Util.udevIsPureUaccessRuleFile(fullfn):
                    self.infoPrinter.printError("\"%s\" is not a pure uaccess udev rule file." % (fullfn))

    def _checkCcacheFilesAndDirectories(self):
        c = CcacheLocalService()
        if not c.is_enabled():
            return

        if c.get_ccache_dir() != FmConst.ccacheDir:
            self.infoPrinter.printError("invalid ccache directory")
            return

        s = os.stat(c.get_ccache_dir())
        if not stat.S_ISDIR(s.st_mode):
            self.infoPrinter.printError("\"%s\" is not a directory" % (c.get_ccache_dir()))
        if s.st_mode != 0o40700:
            self.infoPrinter.printError("invalid mode for \"%s\"" % (c.get_ccache_dir()))
        if s.st_uid != os.getuid():
            self.infoPrinter.printError("invalid owner for \"%s\"" % (c.get_ccache_dir()))
        if s.st_gid != os.getgid():
            self.infoPrinter.printError("invalid group for \"%s\"" % (c.get_ccache_dir()))

    def _checkSystemServiceEnablement(self):
        mustEnableServiceList = [
            "kmod-static-nodes",
            "kmod-no-static-nodes",
        ]
        mustEnableServiceList += [          # FIXME
            "iio-sensor-proxy",             # multiplex daemon for iio-sensor
        ]
        self.fpemudOs.update_service_list(mustEnableServiceList)

        cfgDirObj = strict_systemd.cfg.SysCfgDir()
        for s in mustEnableServiceList:
            cfgObj = cfgDirObj.try_get_service(strict_systemd.ServiceType.DAEMON, unit_name="%s.service" % (s))
            if cfgObj is not None and not cfgObj.is_enabled():
                self.infoPrinter.printError("\"%s\" is not enabled." % (s))

    def _checkPortageCfg(self, machine, bFullCheck=True):
        commonDir = os.path.join(FmConst.dataDir, "etc-common")

        ts = gstage4.TargetSettings()
        self.fpemudOs.update_target_settings(ts)

        # FIXME
        ts.install_mask = [
            "/etc/default",                                         # they are not used since we use pkg-fpemud/fpemud-refsystem
            # "/etc/cron.*",                                        # FIXME: move to gstage4 NoCron
            # "/etc/xinetd.d",                                      # FIXME: move to gstage4 NoXinetd
            "/etc/grub.d",
            "/etc/kernel",
        ]

        # check /var/lib/portage
        if not os.path.isdir(FmConst.portageDataDir):
            if self.bAutoFix:
                os.mkdir(FmConst.portageDataDir)
            else:
                raise FmCheckException("\"%s\" is not a directory" % (FmConst.portageDataDir))

        # check /var/cache/portage
        if not os.path.isdir(FmConst.portageCacheDir):
            if self.bAutoFix:
                os.makedirs(FmConst.portageCacheDir, exist_ok=True)
            else:
                raise FmCheckException("\"%s\" is not a directory" % (FmConst.portageCacheDir))

        # check /var/cache/portage/overlay-db
        if not os.path.isdir(FmConst.cloudOverlayDbDir):
            if self.bAutoFix:
                os.makedirs(FmConst.cloudOverlayDbDir, exist_ok=True)
            else:
                raise FmCheckException("\"%s\" is not a directory" % (FmConst.cloudOverlayDbDir))

        # check /var/cache/portage/laymanfiles
        if not os.path.isdir(FmConst.laymanfilesDir):
            if self.bAutoFix:
                os.makedirs(FmConst.laymanfilesDir, exist_ok=True)
            else:
                raise FmCheckException("\"%s\" is not a directory" % (FmConst.laymanfilesDir))

        # check /var/cache/portage/distfiles
        if not os.path.isdir(FmConst.distDir):
            if self.bAutoFix:
                os.makedirs(FmConst.distDir, exist_ok=True)
            else:
                raise FmCheckException("\"%s\" is not a directory" % (FmConst.distDir))

        # check /etc/portage
        if True:
            portageConfigDir = strict_portage.cfg.PortageConfigDir()

            def __errCb(msg):
                raise FmCheckException(msg)

            checker = portageConfigDir.create_checker(auto_fix=self.bAutoFix, error_callback=__errCb)
            checker.check_self()

            # check /etc/portage/make.profile
            if self.bInit:
                checker.check_make_profile_link(gentoo_repository_dir_path=EbuildRepositories().getRepoDir("gentoo"), profile=FmConst.portageProfile)
            else:
                checker.check_make_profile_link(gentoo_repository_dir_path=EbuildRepositories().getRepoDir("gentoo"), fallback_profile=FmConst.portageProfile)

            # check /etc/portage/make.conf
            if True:
                makeConf = portageConfigDir.get_make_conf_obj()
                with makeConf.create_checker(auto_fix=self.bAutoFix, error_callback=__errCb) as c:
                    c.check_file()

                # check/fix ACCEPT_LICENSE variable
                if makeConf.get_var("ACCEPT_LICENSE") != "*":
                    if self.bAutoFix:
                        makeConf.set_var("ACCEPT_LICENSE", "*")
                    else:
                        raise FmCheckException("invalid value of variable ACCEPT_LICENSE in %s" % (FmConst.portageCfgMakeConf))

                # check/fix DISTDIR variable
                if makeConf.get_var("DISTDIR") != FmConst.distDir:
                    if self.bAutoFix:
                        makeConf.set_var("DISTDIR", FmConst.distDir)
                    else:
                        raise FmCheckException("invalid value of variable DISTDIR in %s" % (FmConst.portageCfgMakeConf))

                # check/fix ACCEPT_KEYWORDS variable
                pkgwh = PkgWarehouse()
                keywordList = ["~%s" % (x) for x in pkgwh.getKeywordList()]
                if set(makeConf.get_var("ACCEPT_KEYWORDS", parse=True)) != set(keywordList):
                    if self.bAutoFix:
                        makeConf.set_var("ACCEPT_KEYWORDS", keywordList, synthesize=True)
                    else:
                        raise FmCheckException("invalid value of variable ACCEPT_KEYWORDS in %s" % (FmConst.portageCfgMakeConf))

                # check/fix MAKEOPTS variable
                # for bug 559064 and 592660, we need to add -j and -l, it sucks
                # "jobcount" and "loadavg" is for emerge executed by user, not for emerge executed by fpemud-os-sysman
                jobcount, loadavg = Util.portageDetectJobCountAndLoadAverage(machine)
                value = makeConf.get_var("MAKEOPTS")
                if True:
                    m = re.search("\\B--jobs(=([0-9]+))?\\b", value)
                    if m is None:
                        if self.bAutoFix:
                            value += " --jobs=%d" % (jobcount)
                            makeConf.set_var("MAKEOPTS", value.lstrip())
                        else:
                            raise FmCheckException("variable MAKEOPTS in %s should contain --jobs argument" % (FmConst.portageCfgMakeConf))
                    elif m.group(2) is None or int(m.group(2)) != jobcount:
                        if self.bAutoFix:
                            value = value.replace(m.group(0), "--jobs=%d" % (jobcount))
                            makeConf.set_var("MAKEOPTS", value.lstrip())
                        else:
                            raise FmCheckException("variable MAKEOPTS in %s has an inappropriate --jobs argument" % (FmConst.portageCfgMakeConf))
                if True:
                    m = re.search("\\B--load-average(=([0-9\\.]+))?\\b", value)
                    if m is None:
                        if self.bAutoFix:
                            value += " --load-average=%d" % (loadavg)
                            makeConf.set_var("MAKEOPTS", value.lstrip())
                        else:
                            raise FmCheckException("variable MAKEOPTS in %s should contain --load-average argument" % (FmConst.portageCfgMakeConf))
                    elif m.group(2) is None or int(m.group(2)) != loadavg:
                        if self.bAutoFix:
                            value = value.replace(m.group(0), "--load-average=%d" % (loadavg))
                            makeConf.set_var("MAKEOPTS", value.lstrip())
                        else:
                            raise FmCheckException("variable MAKEOPTS in %s has an inappropriate --load-average argument" % (FmConst.portageCfgMakeConf))
                if True:
                    m = re.search("\\B-j([0-9]+)?\\b", value)
                    if m is None:
                        if self.bAutoFix:
                            value += " -j%d" % (jobcount)
                            makeConf.set_var("MAKEOPTS", value.lstrip())
                        else:
                            raise FmCheckException("variable MAKEOPTS in %s should contain -j argument" % (FmConst.portageCfgMakeConf))
                    elif m.group(1) is None or int(m.group(1)) != jobcount:
                        if self.bAutoFix:
                            value = value.replace(m.group(0), "-j%d" % (jobcount))
                            makeConf.set_var("MAKEOPTS", value.lstrip())
                        else:
                            raise FmCheckException("variable MAKEOPTS in %s has an inappropriate -j argument" % (FmConst.portageCfgMakeConf))
                if True:
                    m = re.search("\\B-l([0-9]+)?\\b", value)
                    if m is None:
                        if self.bAutoFix:
                            value += " -l%d" % (loadavg)
                            makeConf.set_var("MAKEOPTS", value.lstrip())
                        else:
                            raise FmCheckException("variable MAKEOPTS in %s should contain -l argument" % (FmConst.portageCfgMakeConf))
                    elif m.group(1) is None or int(m.group(1)) != loadavg:
                        if self.bAutoFix:
                            value = value.replace(m.group(0), "-l%d" % (loadavg))
                            makeConf.set_var("MAKEOPTS", value.lstrip())
                        else:
                            raise FmCheckException("variable MAKEOPTS in %s has an inappropriate -l argument" % (FmConst.portageCfgMakeConf))

                # check/fix EMERGE_DEFAULT_OPTS variable
                value = makeConf.get_var("EMERGE_DEFAULT_OPTS")
                if re.search("--quiet-build\\b", value) is None:
                    if self.bAutoFix:
                        value += " --quiet-build"
                        makeConf.set_var("EMERGE_DEFAULT_OPTS", value.lstrip())
                    else:
                        raise FmCheckException("variable EMERGE_DEFAULT_OPTS in %s should contain --quiet-build argument" % (FmConst.portageCfgMakeConf))
                if re.search("--verbose-conflicts\\b", value) is None:
                    if self.bAutoFix:
                        value += " --verbose-conflicts"
                        makeConf.set_var("EMERGE_DEFAULT_OPTS", value.lstrip())
                    else:
                        raise FmCheckException("variable EMERGE_DEFAULT_OPTS in %s should contain --verbose-conflicts argument" % (FmConst.portageCfgMakeConf))
                if True:
                    m = re.search("--backtrack(=([0-9]+))?\\b", value)
                    if m is None:
                        if self.bAutoFix:
                            value += " --backtrack=%d" % (30)
                            makeConf.set_var("EMERGE_DEFAULT_OPTS", value.lstrip())
                        else:
                            raise FmCheckException("variable EMERGE_DEFAULT_OPTS in %s should contain --backtrack argument" % (FmConst.portageCfgMakeConf))
                    elif m.group(2) is None or int(m.group(2)) < 30:
                        if self.bAutoFix:
                            value = value.replace(m.group(0), "--backtrack=%d" % (30))
                            makeConf.set_var("EMERGE_DEFAULT_OPTS", value)
                        else:
                            raise FmCheckException("variable EMERGE_DEFAULT_OPTS in %s has an inappropriate --backtrack argument" % (FmConst.portageCfgMakeConf))
                if True:
                    m = re.search("\\B--jobs(=([0-9]+))?\\b", value)
                    if m is not None:
                        if self.bAutoFix:
                            value = value.replace(m.group(0), "").replace("  ", " ")
                            makeConf.set_var("EMERGE_DEFAULT_OPTS", value.lstrip())
                        else:
                            raise FmCheckException("variable EMERGE_DEFAULT_OPTS in %s should not contain --jobs argument" % (FmConst.portageCfgMakeConf))
                if True:
                    m = re.search("\\B--load-average(=([0-9\\.]+))?\\b", value)
                    if m is not None:
                        if self.bAutoFix:
                            value = value.replace(m.group(0), "").replace("  ", " ")
                            makeConf.set_var("EMERGE_DEFAULT_OPTS", value.lstrip())
                        else:
                            raise FmCheckException("variable EMERGE_DEFAULT_OPTS in %s should not contain --load-average argument" % (FmConst.portageCfgMakeConf))

            # check /etc/portage/repos.conf directory
            if True:
                reposConf = portageConfigDir.get_repos_conf_obj(file_or_dir=False)
                c = reposConf.create_checker(auto_fix=self.bAutoFix, error_callback=__errCb)
                c.check_self()
                # don't finalize, we keep extra files

            # check /etc/portage/repo.postsync.d directory
            if len(ts.repo_postsync_patch_directories) > 0:
                scriptFn = "10-" + os.path.basename(gstage4.REPO_POSTSYNC_PATCH_SCRIPT)
                scriptDirFn = scriptFn + ".d"
                reposPostSyncDir = portageConfigDir.get_repo_postsync_dir_obj()
                with reposPostSyncDir.create_checker(auto_fix=self.bAutoFix, error_callback=__errCb) as c:
                    c.check_self()
                    c.check_member_link(scriptFn, target=gstage4.REPO_POSTSYNC_PATCH_SCRIPT)
                    c.check_extra_dir(scriptDirFn)
                    self.fpemudOs.checkPortageConfigDirByTargetSettings(reposPostSyncDir, c, ts, self.bAutoFix, __errCb)
            else:
                checker.disallow_repo_postsync_dir()

            # check /etc/portage/package.accept_keywords directory
            if True:
                packageAcceptKeywords = portageConfigDir.get_package_accept_keywords_obj(file_or_dir=False)
                with packageAcceptKeywords.create_checker(auto_fix=self.bAutoFix, error_callback=__errCb) as c:
                    c.check_self()
                    c.check_member_link("?-base", os.path.join(commonDir, "package.accept_keywords"))       # standard files
                    c.check_member_file("98-autokeyword-manual")                                            # /etc/portage/package.accept_keywords/98-autokeyword-manual
                    c.check_member_file("99-autokeyword", "")                                               # /etc/portage/package.accept_keywords/99-autokeyword

            # check /etc/portage/package.license file
            if len(ts.pkg_license) > 0:
                packageLicense = portageConfigDir.get_package_license_obj(file_or_dir=True)
                with packageLicense.create_checker(auto_fix=self.bAutoFix, error_callback=__errCb) as c:
                    self.fpemudOs.checkPortageConfigDirByTargetSettings(packageLicense, c, ts, self.bAutoFix, __errCb)
            else:
                checker.disallow_package_license_file_or_dir()
            assert len(ts.pkg_license_files) == 0

            # check /etc/portage/package.mask directory
            if True:
                packageMask = portageConfigDir.get_package_mask_obj(file_or_dir=False)
                with packageMask.create_checker(auto_fix=self.bAutoFix, error_callback=__errCb) as c:
                    c.check_self()
                    self.fpemudOs.checkPortageConfigDirByTargetSettings(packageMask, c, ts, self.bAutoFix, __errCb)
                    c.check_member_file("98-manual")            # /etc/portage/package.mask/98-manual

                # check package atom validity
                if bFullCheck:
                    pass
                    # porttree = portage.db[portage.root]["porttree"]
                    # cpvAll = porttree.dbapi.cpv_all()
                    # for fn in os.listdir(FmConst.portageCfgMaskDir):
                    #     fullfn = os.path.join(FmConst.portageCfgMaskDir, fn)
                    #     for pkgAtom in Util.portageReadCfgMaskFile(fullfn):
                    #         if len(porttree.dbapi.match(pkgAtom)) == 0:
                    #             raise FmCheckException("invalid package atom \"%s\" in %s" % (pkgAtom, fullfn))

            # check /etc/portage/package.unmask directory
            if True:
                packageUnmask = portageConfigDir.get_package_unmask_obj(file_or_dir=False)
                with packageUnmask.create_checker(auto_fix=self.bAutoFix, error_callback=__errCb) as c:
                    c.check_self()
                    c.check_member_file("98-manual")            # /etc/portage/package.unmask/98-manual

            # check /etc/portage/package.use directory
            if True:
                packageUse = portageConfigDir.get_package_use_obj(file_or_dir=False)
                c = packageUse.create_checker(auto_fix=self.bAutoFix, error_callback=__errCb)
                if True:
                    c.check_self()
                    c.check_member_link("?-base", os.path.join(commonDir, "package.use.base"))                      # standard files
                    c.check_member_link("?-base_bugfix", os.path.join(commonDir, "package.use.base_bugfix"))        # standard files
                    self.fpemudOs.checkPortageConfigDirByTargetSettings(packageUse, c, ts, self.bAutoFix, __errCb)
                    c.check_member_file("98-autouse-manual")                                                        # /etc/portage/package.use/98-autouse-manual
                    c.check_member_file("99-autouse")                                                               # /etc/portage/package.use/99-autouse

                    fn = packageUse.get_member_obj("99-autouse")
                    for pkgName, useList in fn.get_use_flag_mapping().items():
                        for uf in useList:
                            if uf.startswith("-"):
                                raise FmCheckException("invalid USE flag \"%s\" for package \"%s\" in %s" % (uf, pkgName, fn.path))

                    # don't do finalize yet, we have some extra files that are not easy to calculate

                # check use flag existence
                if bFullCheck:
                    pass
                    # porttree = portage.db[portage.root]["porttree"]
                    # cpvAll = porttree.dbapi.cpv_all()
                    # for fn in os.listdir(FmConst.portageCfgUseDir):
                    #     fullfn = os.path.join(FmConst.portageCfgUseDir, fn)
                    #     for pkgAtom, useList in Util.portageReadCfgUseFile(fullfn):
                    #         if useList[0].endswith(":"):
                    #             # convert EXPAND_USE to normal use flags
                    #             useList = [useList[0][:-1].lower() + "_" + x.lstrip("-") for x in useList[1:] if x != "-*"]
                    #         else:
                    #             useList = [x.lstrip("-") for x in useList]

                    #         if pkgAtom == "*/*":
                    #             for u in useList:
                    #                 bFound = False
                    #                 for cpv in cpvAll:
                    #                     iuseList = porttree.dbapi.aux_get(cpv, ["IUSE"])[0].split()
                    #                     iuseList = [x.lstrip("+") for x in iuseList]
                    #                     if u in iuseList:
                    #                         bFound = True
                    #                         break
                    #                 if bFound:
                    #                     break
                    #             if not bFound:
                    #                 raise FmCheckException("invalid USE flag \"%s\" for \"%s\" in %s" % (u, pkgAtom, fullfn))
                    #         else:
                    #             cpvList = porttree.dbapi.match(pkgAtom)
                    #             if len(cpvList) == 0:
                    #                 raise FmCheckException("invalid package atom \"%s\" in %s" % (pkgAtom, fullfn))
                    #             if Util.portageIsSimplePkgAtom(pkgAtom):
                    #                 cpvList = cpvList[-1:]                     # checks only the latest version for simple package atom (eg: media-video/smplayer)
                    #             for cpv in cpvList:
                    #                 iuseList = porttree.dbapi.aux_get(cpv, ["IUSE"])[0].split()
                    #                 iuseList = [x.lstrip("+") for x in iuseList]
                    #                 for u in useList:
                    #                     if u not in iuseList:
                    #                         print(iuseList)
                    #                         raise FmCheckException("invalid USE flag \"%s\" for package atom \"%s\" in %s" % (u, pkgAtom, fullfn))

                # check use flag conflict
                if bFullCheck:
                    pass

            # check /etc/portage/package.env and /etc/portage/env directory
            # FIXME
            if True:
                packageEnv = portageConfigDir.get_package_env_obj(file_or_dir=False)
                with packageEnv.create_checker(auto_fix=self.bAutoFix, error_callback=__errCb) as c:
                    c.check_self()
                    self.fpemudOs.checkPortageConfigDirByTargetSettings(packageEnv, c, ts, self.bAutoFix, __errCb)

            # check dispatch-conf.conf
            if True:
                buf = ""
                with open(FmConst.cfgDispatchConf, "r") as f:
                    buf = f.read()

                # check the existence of variable "archive-dir"
                m = re.search("^archive-dir=(.*)$", buf, re.M)
                if m is None:
                    raise FmCheckException("no config item archive-dir in %s" % (FmConst.cfgDispatchConf))

                # check/fix value for variable "archive-dir"
                if m.group(1) != os.path.join("${EPREFIX}", FmConst.configArchiveDir):
                    if self.bAutoFix:
                        newLine = "archive-dir=%s" % (os.path.join("${EPREFIX}", FmConst.configArchiveDir))
                        buf = buf.replace(m.group(0), newLine)
                        with open(FmConst.cfgDispatchConf, 'w') as f:
                            f.write(buf)
                    else:
                        raise FmCheckException("invalid value of config item archive-dir in %s" % (FmConst.cfgDispatchConf))

            # check /etc/portage/cert.pem & /etc/porage/privkey.pem
            if not os.path.exists(FmConst.myCertFile) or not os.path.exists(FmConst.myPrivKeyFile):
                if self.bAutoFix:
                    cert, key = Util.genSelfSignedCertAndKey("-", FmConst.myKeySize)
                    Util.dumpCertAndKey(cert, key, FmConst.myCertFile, FmConst.myPrivKeyFile)
                else:
                    raise FmCheckException("\"%s\" or \"%s\" does not exist" % (FmConst.myCertFile, FmConst.myPrivKeyFile))

            # check /etc/portage/bbki.kernel
            if os.path.exists(FmConst.bbkiKernelFile):
                checker.use_and_check_extra_file(FmConst.bbkiKernelFile, checker=BbkiCfgFileUtil.checkKernelCfgFileContent)

            # check /etc/portage/bbki.kernel_addon directory
            if os.path.exists(FmConst.bbkiKernelAddonDir):
                checker.use_and_check_extra_dir(FmConst.bbkiKernelAddonDir)
                for fn in os.listdir(FmConst.bbkiKernelAddonDir):
                    fullfn = os.path.join(FmConst.bbkiKernelAddonDir, fn)
                    checker.use_and_check_extra_file(fullfn, checker=BbkiCfgFileUtil.checkKernelAddonCfgFileContent)

            # check /etc/portage/bashrc & /etc/portage/bashrc.d
            checker.use_and_check_extra_file(os.path.join(portageConfigDir.path, "bashrc"))
            checker.use_and_check_extra_dir(os.path.join(portageConfigDir.path, "bashrc.d"))

            # redundant files
            # FIXME
            # if bFullCheck:
            #     checker.finalize()

    def _checkCupsCfg(self):
        cfg = strict_cups.CupsCfg("2.4.0")
        if isinstance(self.fpemudOs.ftPrinting, gstage4.target_features.UsePrinting):
            if not cfg.exists():
                self.infoPrinter.printError("CUPS config does not exist.")
            cfg.check(auto_fix=self.bAutoFix, error_callback=self.infoPrinter.printError)
            if len(cfg.get_classes()) > 0:
                self.infoPrinter.printError("There should be no class in CUPS config.")
        elif isinstance(self.fpemudOs.ftPrinting, gstage4.target_features.DontUsePrinting):
            if cfg.exists():
                self.infoPrinter.printError("CUPS config should not exist.")
        else:
            assert False

    def _checkRepositories(self, bFullCheck=True):
        """Check repositories"""

        repoman = EbuildRepositories()

        # all repositories should exist
        for repoName in repoman.getRepositoryList():
            if not repoman.isRepoExist(repoName):
                if self.bAutoFix:
                    repoman.createRepository(repoName)
                else:
                    raise FmCheckException("repository \"%s\" does not exist" % (repoName))

        # check all repositories
        for repoName in repoman.getRepositoryList():
            # FIXME: this function throws exception directly, there should be a way to auto fix it
            try:
                repoman.checkRepository(repoName, self.bAutoFix)
            except RepositoryCheckError as e:
                raise FmCheckException(e.message)

        # basic check stops here
        if not bFullCheck:
            return

        # there should be no same ebuild directory between repositories
        infoDict = dict()
        for repoName in repoman.getRepositoryList():
            repoDir = repoman.getRepoDir(repoName)
            infoDict[repoName] = set(Util.repoGetEbuildDirList(repoDir))

        tlist = list(infoDict.items())
        for i in range(0, len(tlist)):
            for j in range(i + 1, len(tlist)):
                k, v = tlist[i]
                k2, v2 = tlist[j]
                for vi in list(v & v2):
                    self.infoPrinter.printError("Repository \"%s\" and \"%s\" has same package \"%s\"" % (k, k2, vi))

    def _checkOverlays(self, bFullCheck=True):
        """Check overlays"""

        repoman = EbuildRepositories()
        layman = EbuildOverlays()

        # check all overlays
        for overlayName in layman.getOverlayList():
            try:
                layman.checkOverlay(overlayName, True, self.bAutoFix)
            except OverlayCheckError as e:
                raise FmCheckException(e.message)

        # basic check stops here
        if not bFullCheck:
            return

        overlayDb = CloudOverlayDb()
        if not overlayDb.isUpdateComplete():
            if self.bAutoFix:
                overlayDb.update()
            else:
                raise FmCheckException("overlay database is not updated")

        for overlayName in layman.getOverlayList():
            oDir = layman.getOverlayDir(overlayName)

            # 1. compare overlay vcs-type and name with cloud database, use cache only
            if overlayDb.hasOverlay(overlayName):
                ret = overlayDb.getOverlayVcsTypeAndUrl(overlayName)
                if ret != layman.getOverlayVcsTypeAndUrl(overlayName):
                    if self.bAutoFix:
                        layman.modifyOverlayVcsTypeAndUrl(overlayName, ret[0], ret[1])
                    else:
                        self.infoPrinter.printError("Overlay \"%s\" should have VCS type \"%s\" and URL \"%s\", same with the cloud overlay database." % (overlayName, ret[0], ret[1]))

            # 2. there should be no same ebuild directory between repository and overlay
            if True:
                infoDict = dict()
                for repoName in repoman.getRepositoryList():
                    repoDir = repoman.getRepoDir(repoName)
                    infoDict[repoName] = set(Util.repoGetEbuildDirList(repoDir))

                oDirInfo = set(Util.repoGetEbuildDirList(oDir))
                for k, v in infoDict.items():
                    for vi in list(v & oDirInfo):
                        if self.bAutoFix and layman.getOverlayType(overlayName) in ["trusted", "transient"]:
                            Util.repoRemovePackageAndCategory(oDir, vi)
                        else:
                            self.infoPrinter.printError("Repository \"%s\" and overlay \"%s\" has same package \"%s\"." % (k, overlayName, vi))

            # 3. transient overlay must has at least one enabled package
            #    this should be after check 2 since check 2 may auto remove package from overlay
            if layman.getOverlayType(overlayName) == "transient":
                if len(Util.repoGetEbuildDirList(oDir)) == 0:
                    if self.bAutoFix:
                        layman.removeOverlay(overlayName)
                        continue
                    else:
                        self.infoPrinter.printError("Overlay \"%s\" has no enabled package." % (overlayName))

            # 4. overlays should not have same repo_name
            if True:
                overlayRepoName = layman.getOverlayMetadata(overlayName, "repo-name")
                for repoName in repoman.getRepositoryList():
                    if repoman.getRepoMetadata(repoName, "repo-name") == overlayRepoName:
                        self.infoPrinter.printError("Repository \"%s\" and overlay \"%s\" has same repo_name." % (repoName, overlayName))
                for oname2 in layman.getOverlayList():
                    if overlayName == oname2:
                        continue
                    if layman.getOverlayMetadata(oname2, "repo-name") == overlayRepoName:
                        self.infoPrinter.printError("Overlay \"%s\" and \"%s\" has same repo_name." % (oname2, overlayName))

#            # there should be no same set files between overlays
#            if True:
#                infoDict = dict()
#                for oname2 in layman.getOverlayList():
#                    oSetDir = os.path.join(layman.getOverlayFilesDir(oname2), "set")
#                    infoDict[oname2] = set(os.listdir(oSetDir)) if os.path.exists(oSetDir) else set()
#                for oname2 in layman.getOverlayList():
#                    if overlayName == oname2:
#                        continue
#                    vi = list(infoDict[overlayName] & infoDict[oname2])
#                    if len(vi) == 0:
#                        continue
#                    for f in vi:
#                        self.infoPrinter.printError("Overlay \"%s\" and \"%s\" has same set file \"%s\"" % (overlayName, oname2, f))
#                    for f in vi:
#                        fname1 = os.path.join(layman.getOverlayFilesDir(k), "set", f)
#                        fname2 = os.path.join(layman.getOverlayFilesDir(k2), "set", f)
#                        if not Util.fileHasSameContent(fname1, fname2):
#                            raise FmCheckException("overlay \"%s\" and \"%s\" has same set file \"%s\"" % (k, k2, f))

    def _checkRedundantRepositoryAndOverlay(self):
        repoman = EbuildRepositories()
        layman = EbuildOverlays()

        tlist = []
        tlist += glob.glob(os.path.join(FmConst.portageDataDir, "repo-*"))
        tlist += glob.glob(os.path.join(FmConst.portageDataDir, "overlay-*"))

        # check /etc/portage/repos.conf directory
        fileList = []
        for repoName in repoman.getRepositoryList():
            fileList.append(repoman.getRepoCfgReposFile(repoName))
        for overlayName in layman.getOverlayList():
            fileList.append(layman.getOverlayCfgReposFile(overlayName))
        for fn in os.listdir(FmConst.portageCfgReposDir):
            fullfn = os.path.join(FmConst.portageCfgReposDir, fn)
            if fullfn not in fileList:
                if self.bAutoFix:
                    os.remove(fullfn)
                else:
                    self.infoPrinter.printError("Redundant repository configuration file \"%s\" exist." % (fullfn))

        # find un-referenced directory in /var/lib/portage/repo-* & /var/lib/portage/overlay-*
        redundantList = []
        for fullfn in tlist:
            fn = os.path.basename(fullfn)
            if not os.path.exists(os.path.join(FmConst.portageCfgReposDir, "%s.conf" % (fn))):
                if self.bAutoFix:
                    Util.forceDelete(fullfn)
                else:
                    redundantList.append(fullfn)
                    self.infoPrinter.printError("Redundant directory \"%s\" found." % (fullfn))

        # find un-referenced directory in /var/cache/portage/laymanfiles/*
        for fn in os.listdir(FmConst.laymanfilesDir):
            fullfn = os.path.join(FmConst.laymanfilesDir, fn)
            libFullfn = os.path.join(FmConst.portageDataDir, "overlay-%s" % (fn))
            if not os.path.exists(libFullfn) or libFullfn in redundantList:
                if self.bAutoFix:
                    Util.forceDelete(fullfn)
                else:
                    self.infoPrinter.printError("Redundant directory \"%s\" found." % (fullfn))

    def _checkNews(self):
        # check unread news
        if Util.cmdCall("eselect", "news", "count") != "0":
            if self.bAutoFix:
                subprocess.run(["eselect", "news", "read", "all"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            else:
                self.infoPrinter.printError("There are unread portage news items, please use \"eselect news read all\".")

    def _checkPortagePkgwhCfg(self, machine):
        pkgwh = PkgWarehouse()

        # /etc/portage/package.use/30-hardware
        if self.bAutoFix:
            pkgwh.refreshHardwareUseFlags(machine)
        else:
            pkgwh.checkHardwareUseFlags(machine)

        # /etc/portage/package.use/40-linguas
        # FIXME: support syncupd
        if self.bAutoFix:
            pkgwh.refreshLinguasUseFlags()
        else:
            pkgwh.checkLinguasUseFlags()

        # python version selection and USE flags
        ver = pkgwh.pythonGetCurrentVersion()
        if ver is not None:
            # does USE flags match the current python version?
            errMsgList = pkgwh.pythonCheckVersionMatching(ver)
            for errMsg in errMsgList:
                # no way to auto fix
                self.infoPrinter.printError(errMsg)

            # does the valid newest version has been selected?
            newestVer = pkgwh.pythonGetValidNewestVersion(ver)
            if newestVer != ver:
                # should not auto fix
                self.infoPrinter.printError("Python version (%s) is not the latest one (%s)." % (ver, newestVer))

            # /etc/portage/package.use/90-python-targets
            if self.bAutoFix:
                pkgwh.pythonRefreshTargetUseFlags()
            else:
                pkgwh.pythonCheckTargetUseFlags()

            # /etc/python-exec/python-exec.conf
            try:
                fullfn = "/etc/python-exec/python-exec.conf"
                lineList = pathlib.Path(fullfn).read_text().split("\n")
                lineList = [x.strip() for x in lineList]
                lineList = [x for x in lineList if x != "" and not x.startswith("#")]
                if len(lineList) > 1:
                    if self.bAutoFix:
                        pathlib.Path(fullfn).write_text(lineList[0] + "\n")
                    else:
                        self.infoPrinter.printError("File \"%s\" contains multiple versions." % (fullfn))
            except FileNotFoundError:
                pass

        # ruby version selection and USE flags
        ver = pkgwh.rubyGetCurrentVersion()
        if ver is not None:
            # does USE flags match the current ruby version?
            errMsgList = pkgwh.rubyCheckVersionMatching(ver)
            for errMsg in errMsgList:
                # no way to auto fix
                self.infoPrinter.printError(errMsg)

            # does the valid newest version has been selected?
            newestVer = pkgwh.rubyGetValidNewestVersion(ver)
            if newestVer != ver:
                # should not auto fix
                self.infoPrinter.printError("Ruby version (%s) is not the latest one (%s)." % (ver, newestVer))

            # /etc/portage/package.use/90-ruby-targets
            if self.bAutoFix:
                pkgwh.rubyRefreshTargetUseFlags()
            else:
                pkgwh.rubyCheckTargetUseFlags()

        # wine version selection
        ver = pkgwh.wineGetCurrentVersion()
        if ver is not None:
            # does the valid newest version has been selected?
            newestVer = pkgwh.wineGetNewestVersion()
            if newestVer != ver:
                # should not auto fix
                self.infoPrinter.printError('Wine version (%s) is not the latest one (%s), you may use "eselect wine update".' % (ver, newestVer))

        # depencencies for USE flags, should be the last one in this function
        if self.bAutoFix:
            pkgwh.refreshUseFlagDeps()
        else:
            pkgwh.checkUseFlagDeps()

        # package sets for /home/*/maint/*
        pkgwh.checkMaintPkgSets(self.bAutoFix, self.infoPrinter.printError)

    def _checkWorldFile(self):
        worldFile = strict_portage.cfg.World()
        if len(worldFile.get_set_names()) > 0:
            self.infoPrinter.printError("There should be no package set in @world.")

        realWorldSet = set()
        for pkg in strict_portage.cfg.World().get_package_names():
            if not Util.portageIsPkgInstallable(pkg):
                self.infoPrinter.printError("Uninstallable package \"%s\" in \"%s\"." % (pkg, worldFile.world_filepath))
                continue
            realWorldSet.add(pkg)

        worldSet = set()
        self.fpemudOs.update_world_set(worldSet)

        for pkg in worldSet - realWorldSet:
            self.infoPrinter.printError("Package \"%s\" not in \"%s\"." % (pkg, worldFile.world_filepath))

    def _checkUsersAndGroups(self):
        # make sure passwd/group/shadow are tidy
        if self.bAutoFix:
            with strict_pgs.PasswdGroupShadow(readOnly=False, msrc="fpemud-os") as pgs:
                pass

        # do checks
        with strict_pgs.PasswdGroupShadow(msrc="fpemud-os") as pgs:
            pgs.verify()

            for uname in pgs.getNormalUserList():
                homeDir = pwd.getpwnam(uname).pw_dir
                if not os.path.isdir(homeDir):
                    self.infoPrinter.printError("Normal user %s has no home directory." % (uname))
                # other properties of home direcotry is checked in self._checkRootfsLayout()

            for uname in pgs.getSystemUserList():
                if uname == "root":
                    homeDir = pwd.getpwnam(uname).pw_dir
                    if homeDir != "/root":
                        self.infoPrinter.printError("System user %s has invalid home directory \"%s\"." % (uname, homeDir))
                    shell = pwd.getpwnam(uname).pw_shell
                    if shell != "/bin/sh":
                        self.infoPrinter.printError("System user %s has invalid shell \"%s\"." % (uname, shell))
                elif uname == "nobody":
                    homeDir = pwd.getpwnam(uname).pw_dir
                    if homeDir != "/var/empty":
                        self.infoPrinter.printError("System user %s has invalid home directory \"%s\"." % (uname, homeDir))
                else:
                    if pwd.getpwnam(uname).pw_dir != "/dev/null":
                        self.infoPrinter.printError("System user %s should not have home directory." % (uname))

            if len(pgs.getRedundantUserList()) > 0:
                self.infoPrinter.printError("There should be no redundant users.")
            if len(pgs.getRedundantGroupList()) > 0:
                self.infoPrinter.printError("There should be no redundant groups.")

    def _checkSystemLocale(self):
        """Check system locale configuration"""

        fn = "/etc/locale.conf"
        content = "LANG=\"C.utf8\""     # we don't accept LANGUAGE and LC_* variables

        # check if /etc/locale.conf exists
        if not os.path.exists(fn):
            if not self.bAutoFix:
                self.infoPrinter.printError("Locale is not configured.")
            else:
                with open(fn, "w") as f:
                    f.write(content + "\n")
            return

        # check if content of /etc/locale.conf is correct
        lines = Util.readListFile(fn)
        if len(lines) != 1 or lines[0] != content:
            if not self.bAutoFix:
                self.infoPrinter.printError("System locale should be configured as \"C.utf8\".")
            else:
                with open(fn, "w") as f:
                    f.write(content + "\n")
            return

    def _checkSystemService(self):
        # all enabled services should be runnning
        # FIXME: not that simple
        if False:
            for s in Util.systemdGetAllServicesEnabled():
                print(s)
                if not Util.systemdIsUnitRunningByDbus(s):
                    self.infoPrinter.printError("Service \"%s\" is enabled but not running.")

        # default LimitNOFILE is 1024:524288 (for soft and hard limit)
        # ideally it should be infinity:infinity
        # we make it 524288:524288 currently
        cfg = configparser.ConfigParser()
        cfg.read("/etc/systemd/system.conf")
        if not cfg.has_option("Manager", "DefaultLimitNOFILE") or cfg.get("Manager", "DefaultLimitNOFILE") != "524288:524288":
            self.infoPrinter.printError("DefaultLimitNOFILE in /etc/systemd/system.conf should be set to 524288.")

        # check limit in-effect for all system services
        pass

    def _checkMrget(self):
        if len(glob.glob("/usr/lib/python*/site-packages/mrget")) == 0:
            # mrget is not installed
            return

        cfgPath = "/etc/mrget/mrget.conf"

        nbuf = ''
        nbuf += '[backend:public-mirror-db]\n'
        nbuf += '{\n'
        nbuf += '    "type": "http-dir"\n'
        nbuf += '    "param": {\n'
        nbuf += '        "urls": [\n'
        nbuf += '            "https://public-mirror-db.srht.site",\n'
        nbuf += '            "https://public-mirror-db.codeberg.page"\n'
        nbuf += '        ],\n'
        nbuf += '        "aging-time": 86400\n'
        nbuf += '    }\n'
        nbuf += '}\n'

        def __readCfgFile():
            sectionDict = {}
            try:
                buf = pathlib.Path(cfgPath).read_text()
                currentSection = None
                for line in buf.split("\n") + [""]:
                    if currentSection is None:
                        line = line.strip()
                        if line == "" or line.startswith('#'):
                            continue
                        if line.startswith('[') and line.endswith(']'):
                            currentSection = line[1:-1]
                            currentSectionContent = ""
                            continue
                        return (False, False, "%s content is invalid." % (cfgPath))
                    else:
                        if len(line.strip()) != 0:
                            currentSectionContent += line
                        else:
                            try:
                                sectionDict[currentSection] = json.loads(currentSectionContent)
                            except json.JSONDecodeError:
                                return (False, False, "%s content is invalid." % (cfgPath))
                            currentSection = None
                            del currentSectionContent
                if currentSection is not None:
                    return (False, False, "%s content is invalid." % (cfgPath))
                return (True, False, (buf, sectionDict))
            except FileNotFoundError:
                return (False, True, "%s is not found." % (cfgPath))

        bSuccess, bReCreate, ret = __readCfgFile()
        if not bSuccess:
            if bReCreate and self.bAutoFix:
                pathlib.Path(cfgPath).write_text(nbuf)
            else:
                self.infoPrinter.printError(ret)
        else:
            buf, sectionDict = ret
            if "backend:public-mirror-db" not in sectionDict:
                if self.bAutoFix:
                    pathlib.Path(cfgPath).write_text(buf.rstrip("\n") + "\n\n" + nbuf)
                else:
                    self.infoPrinter.printError('%s has no "public-mirror-db" mirror.' % (cfgPath))
            else:
                data = sectionDict["backend:public-mirror-db"]
                bPass = True
                if bPass and data.get("type", None) != "http-dir":
                    bPass = False
                if bPass and "param" not in data:
                    bPass = False
                if bPass and set(data["param"].get("urls", [])) != set(["https://public-mirror-db.srht.site", "https://public-mirror-db.codeberg.page"]):
                    bPass = False
                if bPass and data["param"].get("aging-time", None) != 86400:
                    bPass = False
                if not bPass:
                    if self.bAutoFix:
                        os.makedirs(os.path.dirname(cfgPath), exist_ok=True)
                        tmpfn = cfgPath + ".tmp"
                        with open(tmpfn, "w") as f:
                            for sectionName, sectionData in sectionDict.items():
                                if sectionName != "backend:public-mirror-db":
                                    f.write("[%s]\n" % (sectionName))
                                    f.write(json.dumps(sectionData, indent=4) + "\n")
                                    f.write("\n")
                            f.write(nbuf)
                        os.rename(tmpfn, cfgPath)
                    else:
                        self.infoPrinter.printError('%s has invalid "public-mirror-db" mirror.' % (cfgPath))

    def _checkSystemTime(self):
        # check timezone configuration
        while True:
            if not os.path.exists("/etc/timezone") or not os.path.exists("/etc/localtime"):
                self.infoPrinter.printError("Timezone is not properly configured.")
                break
            tz = None
            with open("/etc/timezone", "r") as f:
                tz = os.path.join("/usr/share/zoneinfo", f.read().rstrip("\n"))
            if not os.path.exists(tz):
                self.infoPrinter.printError("Timezone is not properly configured.")
                break
            if not filecmp.cmp("/etc/localtime", tz):
                self.infoPrinter.printError("Timezone is not properly configured.")
                break
            break

        # check system time
        try:
            for i in range(0, 4):
                try:
                    nc = ntplib.NTPClient()
                    ret = nc.request("%d.pool.ntp.org" % (i))
                    if abs(ret.offset) > 1.0:
                        # we tolerant an offset of 1 seconds
                        self.infoPrinter.printError("System time is incorrect. Maybe you need network time synchronization?")
                    break
                except:
                    if i == 3:
                        raise
        except Exception as e:
            self.infoPrinter.printError("Error occured when checking system time, %s." % (str(e)))

    def _checkUserLinger(self):
        fn = "/var/lib/systemd/linger"
        if os.path.exists(fn) and len(os.listdir(fn)):
            self.infoPrinter.printError("\"%s\" should be empty, we don't use logind user linger function." % (fn))

    def _checkNoKmodStaticNodes(self):
        # we don't use kmod-static-nodes, it's complicate
        # we auto load kernel modules with static node on system boot

        fn = "/run/udev/static_node-tags"
        if os.path.exists(fn):
            self.infoPrinter.printError("\"%s\" should not exist." % (fn))

    def _checkTmpFiles(self):
        # we don't use runtime tmpfiles configuration, it's too complex
        fn = "/run/tmpfiles.d"
        if os.path.exists(fn):
            self.infoPrinter.printError("\"%s\" should not exist." % (fn))

        # tmpfiles can only be used for limited scopes
        prefixList = ["/var/cache", "/var/db", "/var/lib", "/var/log", "/var/spool"]
        for dirpath in ["/etc/tmpfiles.d"]:
            if os.path.isdir(dirpath):
                for fullfn in glob.glob(os.path.join(dirpath, "*.conf")):
                    for line in Util.readListFile(fullfn):
                        line = re.search(r"\S+\s+(\S+)(\s+|$)", line).group(1)
                        if not any([line.startswith(x + "/") for x in prefixList]):
                            self.infoPrinter.printError("Path \"%s\" is disallowed in \"%s\"." % (line, fullfn))
        # it sucks that many packages have temp files in the following directories
        prefixList += ["/run", "/var/tmp", "/tmp"]
        for dirpath in ["/usr/lib/tmpfiles.d"]:
            if os.path.isdir(dirpath):
                for fullfn in glob.glob(os.path.join(dirpath, "*.conf")):
                    for line in Util.readListFile(fullfn):
                        line = re.search(r"\S+\s+(\S+)(\s+|$)", line).group(1)
                        if not any([line.startswith(x + "/") for x in prefixList]):
                            self.infoPrinter.printError("Path \"%s\" is disallowed in \"%s\"." % (line, fullfn))

    def _checkSysUsers(self):
        # we don't use runtime sysusers configuration, it's too complex
        fn = "/run/sysusers.d"
        if os.path.exists(fn):
            self.infoPrinter.printError("\"%s\" should not exist." % (fn))

    def _checkEnvironmentVariables(self):
        ps = procfs.pidstats()

        def __checkValue(varName, value):
            if envDict.get(varName) != value:
                self.infoPrinter.printError("Environment variable %s for process %d is invalid" % (varName, pid))

        def __checkExist(varName):
            if varName not in envDict:
                self.infoPrinter.printError("Environment variable %s for process %d must exist" % (varName, pid))

        for pid, v in ps.processes.items():
            # ignore kernel threads
            if "PF_KTHREAD" in v["stat"].process_flags():
                continue

            # ignore user process that are started by kernel (not started by init), for example bpfilter_umh
            if pid != 1:
                final_ppid = pid
                while ps.processes[final_ppid]["stat"]["ppid"] != 0:
                    final_ppid = ps.processes[final_ppid]["stat"]["ppid"]
                if final_ppid != 1:
                    continue

            # procfs module would raise exception when garbage is in /proc/X/environ (maybe caused by memory corruption)
            # we do this check to prevent it
            if b'=' not in pathlib.Path("/proc/%d/environ" % (pid)).read_bytes():
                continue

            eff_pid = pid
            if pid != v["status"]["Tgid"]:
                eff_pid = v["status"]["Tgid"]

            envDict = v["environ"]
            if eff_pid == 1:
                __checkValue("HOME", "/")
                __checkExist("TERM")
            else:
                __checkExist("PATH")
                __checkExist("LANG")

    def _checkPkgConfig(self):
        # we don't need pkg-config?
        if not Util.cmdCallTestSuccess("pkg-config", "--version"):
            return

        for item in Util.shellCall("pkg-config --list-package-names").split("\n"):
            if not Util.cmdCallTestSuccess("pkg-config", "--cflags", "--libs", item):
                self.infoPrinter.printError("Invalid build dependency for pkg-config package %s." % (item))

    def _checkBarkspider(self):
        # barkspider is not installed? emm, barkspider is only needed for bad network
        try:
            import barkspider
        except ModuleNotFoundError:
            return

        needRobustifyList = []

        # check, record if we will do auto-fix
        ctx = barkspider.SystemContext()
        for app_name in ctx.get_app_names(installed_only=True):
            app_stat, msg, last_update_time = ctx.get_app_info(app_name)
            if app_stat != barkspider.AppStat.FULLY_ROBUSTIFIED:
                if self.bAutoFix:
                    needRobustifyList.append(app_name)
                else:
                    self.infoPrinter.printError("Application %s is not fully robustified by barkspider." % (app_name))

        if len(needRobustifyList) > 0:
            # auto-fix
            ctx.robustify_app_config(needRobustifyList)

            # re-check after auto-fix
            for app_name in needRobustifyList:
                app_stat, msg, last_update_time = ctx.get_app_info(app_name)
                if app_stat != barkspider.AppStat.FULLY_ROBUSTIFIED:
                    self.infoPrinter.printError("Application %s is not fully robustified by barkspider." % (app_name))

    def _checkPackageContentFile(self, machine, pkgNameVer):
        contf = os.path.join(FmConst.portageDbDir, pkgNameVer, "CONTENTS_2")
        if not os.path.exists(contf):
            if self.bAutoFix:
                PkgMerger(machine).reInstallPkg(pkgNameVer)
                if not os.path.exists(contf):
                    self.infoPrinter.printError("Content file %s is missing, auto-fix failed." % (contf))
            else:
                self.infoPrinter.printError("Content file %s is missing." % (contf))

    def _checkPackageFileScope(self, pkgNameVer):
        # there're some directories and files I think should not belong to any package, but others don't think so...
        wildcards = []
        if True:
            obj = strict_fsh.RootFs()
            wilecards = strict_fsh.merge_wildcards(wildcards, obj.get_wildcards(wildcards_flag=strict_fsh.WILDCARDS_SYSTEM_BOOT))
            wildcards = strict_fsh.merge_wildcards(wildcards, obj.get_wildcards(wildcards_flag=strict_fsh.WILDCARDS_SYSTEM_DATA))
            wildcards = strict_fsh.merge_wildcards(wildcards, obj.get_wildcards(wildcards_flag=strict_fsh.WILDCARDS_SYSTEM_CACHE))
            wildcards = strict_fsh.merge_wildcards(wildcards, obj.get_wildcards(wildcards_flag=strict_fsh.WILDCARDS_USER))
            wildcards = strict_fsh.merge_wildcards(wildcards, obj.get_wildcards(wildcards_flag=strict_fsh.WILDCARDS_RUNTIME))

        # get file list for this package
        cmdStr = r"cat /var/db/pkg/%s/CONTENTS " % (pkgNameVer)
        cmdStr += r'| sed -e "s:^obj \(.*\) [[:xdigit:]]\+ [[:digit:]]\+$:\1:" '
        cmdStr += r'| sed -e "s:^sym \(.*\) -> .* .*$:\1:" '
        cmdStr += r'| sed -e "s:^dir \(.*\)$:\1:" '
        ret = Util.shellCall(cmdStr).split("\n")

        # check
        for fn in strict_fsh.wildcards_filter(ret, wilecards):
            self.infoPrinter.printError("\"%s\" should not be installed by package manager. (add to \"/usr/lib/tmpfiles.d/*.conf\"?)" % (fn))

    def _checkPackageMd5(self, pkgNameVer):
        itemList = []

        # get item list from CONTENTS_2 file
        if True:
            contf = os.path.join(FmConst.portageDbDir, pkgNameVer, "CONTENTS_2")
            if not os.path.exists(contf):
                # FIXME
                self.infoPrinter.printError("CONTENTS_2 file for %s is missing." % (pkgNameVer))
                return
            itemList = Util.portageParseVarDbPkgContentFile(contf)

        # check md5
        for item in itemList:
            if item[0] == "dir":
                if not os.path.exists(item[1]):
                    self.infoPrinter.printError("Directory %s is missing." % (item[1]))
                else:
                    s = os.stat(item[1])
                    if s.st_uid != item[3]:
                        self.infoPrinter.printError("Directory %s failes for uid verification." % (item[1]))
                    if s.st_gid != item[4]:
                        self.infoPrinter.printError("Directory %s failes for gid verification." % (item[1]))
            elif item[0] == "obj":
                if not os.path.exists(item[1]):
                    self.infoPrinter.printError("File %s is missing" % (item[1]))
                else:
                    if not Util.verifyFileMd5(item[1], item[2]):
                        self.infoPrinter.printError("File %s fails for MD5 verification." % (item[1]))
                    s = os.stat(item[1])
                    if s.st_mode != item[3]:
                        self.infoPrinter.printError("File %s failes for permission verification." % (item[1]))
                    if s.st_uid != item[4]:
                        self.infoPrinter.printError("File %s failes for uid verification." % (item[1]))
                    if s.st_gid != item[5]:
                        self.infoPrinter.printError("File %s failes for gid verification." % (item[1]))
            elif item[0] == "sym":
                if not os.path.islink(item[1]):
                    self.infoPrinter.printError("Symlink %s is missing." % (item[1]))
                else:
                    if os.readlink(item[1]) != item[2]:
                        self.infoPrinter.printError("Symlink %s fails for target verification." % (item[1]))
                    if not os.path.exists(item[1]):
                        self.infoPrinter.printError("Symlink %s is broken." % (item[1]))
                    else:
                        s = os.stat(item[1])
                        if s.st_uid != item[3]:
                            self.infoPrinter.printError("Symlink %s failes for uid verification." % (item[1]))
                        if s.st_gid != item[4]:
                            self.infoPrinter.printError("Symlink %s failes for gid verification." % (item[1]))
            else:
                assert False


class FmCheckException(Exception):
    pass
